package com.katesoft.scale4j.persistent.spring;

import static com.katesoft.scale4j.common.io.FileUtility.LINE_SEPARATOR;
import static com.katesoft.scale4j.common.io.FileUtility.path;
import static com.katesoft.scale4j.common.lang.RuntimeUtility.HIBERNATE_DATA_DIR;
import static com.katesoft.scale4j.common.lang.RuntimeUtility.HIBERNATE_SEARCH_DATA_DIR;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
import static org.apache.commons.io.FileUtils.writeStringToFile;
import static org.apache.commons.lang.StringUtils.join;

import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Properties;

import javax.sql.DataSource;

import org.apache.commons.lang.exception.NestableRuntimeException;
import org.hibernate.cfg.Configuration;
import org.hibernate.dialect.Dialect;
import org.hibernate.search.store.FSDirectoryProvider;
import org.hibernate.tool.hbm2ddl.DatabaseMetadata;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.ConnectionCallback;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.init.DatabasePopulator;
import org.springframework.jdbc.support.JdbcUtils;

import com.katesoft.scale4j.common.lang.RuntimeUtility;
import com.katesoft.scale4j.log.LogFactory;
import com.katesoft.scale4j.log.Logger;
import com.katesoft.scale4j.persistent.hibernate.StoreUpdateScriptsConfiguration;

/**
 * @author kate2007
 */
public final class HibernateSessionUtility {
   private static final Logger LOGGER = LogFactory.getLogger(HibernateSessionUtility.class);

   /**
    * enrich original properties with scale4j default properties.
    * 
    * @param original
    *           properties passed by client
    * @return merged collection of properties to be passed to SessionFactory
    */
   public static Properties hibernateProperties(Properties original) {
      Properties properties = new Properties();
      properties.putAll(original);
      if (!properties.contains("hibernate.search.default.indexBase")) {
         properties.put("hibernate.search.default.indexBase", HIBERNATE_SEARCH_DATA_DIR);
      }
      if (!properties.contains("hibernate.search.default.directory_provider")) {
         properties.put("hibernate.search.default.directory_provider",
                  FSDirectoryProvider.class.getName());
      }
      if (!properties.contains("org.hibernate.envers.do_not_audit_optimistic_locking_field")) {
         properties.put("org.hibernate.envers.do_not_audit_optimistic_locking_field",
                  FALSE.toString());
      }
      if (!properties.contains("org.hibernate.envers.store_data_at_delete")) {
         properties.put("org.hibernate.envers.store_data_at_delete", TRUE.toString());
      }
      properties.put("hibernate.search.autoregister_listeners", FALSE.toString());
      return properties;
   }

   /**
    * store schema create/drop scripts on filesystem if necessary(according to
    * updateScriptsConfiguration).
    * 
    * @param configuration
    *           configuration used to build session factory object.
    * @param updateScriptsConfiguration
    *           store what - update/delete/create ?
    */
   public static void storeSchemaCreateDeleteScripts(final Configuration configuration,
            final StoreUpdateScriptsConfiguration updateScriptsConfiguration) {
      Dialect dialect = Dialect.getDialect(configuration.getProperties());
      try {
         String hibernateScriptsFolder = path(HIBERNATE_DATA_DIR, "scripts");
         String timestamp = Long.valueOf(System.currentTimeMillis()).toString();
         String schemaCreationOutput = path(hibernateScriptsFolder, "schema_create", timestamp
                  + ".sql");
         String schemaDropOutput = path(hibernateScriptsFolder, "schema_drop", timestamp + ".sql");
         if (updateScriptsConfiguration.isGenerateSchemaCreateScript()) {
            File file = new File(schemaCreationOutput);
            writeStringToFile(file,
                     join(configuration.generateSchemaCreationScript(dialect), LINE_SEPARATOR));
            LOGGER.info("create schema script dumped to %s ", file.getCanonicalPath());
         }
         if (updateScriptsConfiguration.isGenerateSchemaDropScript()) {
            File file = new File(schemaDropOutput);
            writeStringToFile(file,
                     join(configuration.generateDropSchemaScript(dialect), LINE_SEPARATOR));
            LOGGER.info("drop schema script dumped to %s ", file.getCanonicalPath());
         }
      } catch (IOException e) {
         LOGGER.error(e);
         if (RuntimeUtility.isJunit()) {
            throw new NestableRuntimeException(e);
         }
      }
   }

   /**
    * prepare update script using hibernate metadata lookup and then execute db update.
    * 
    * @param dataSource
    *           db connection to use
    * @param configuration
    *           configuration used to build session factory object.
    * @param updateScriptsConfiguration
    *           store what - update/delete/create ?
    * @param databasePopulator
    *           anything that implements {@link DatabasePopulator} interface.
    */
   public static void performSchemaUpdate(final DataSource dataSource,
            final Configuration configuration,
            final StoreUpdateScriptsConfiguration updateScriptsConfiguration,
            final DatabasePopulator databasePopulator) {
      JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
      if (databasePopulator != null) {
         jdbcTemplate.execute(new ConnectionCallback<Object>() {
            @Override
            public Object doInConnection(Connection con) throws SQLException, DataAccessException {
               LOGGER.info(
                        "performing db population using %s and connection = %s before hibernate schema update",
                        databasePopulator, con);
               try {
                  databasePopulator.populate(con);
               } catch (SQLException e) {
                  LOGGER.error(e);
                  if (RuntimeUtility.isJunit()) {
                     throw e;
                  }
               }
               return null;
            }
         });
      }
      jdbcTemplate.execute(new ConnectionCallback<Object>() {
         @Override
         public Object doInConnection(Connection connection) throws SQLException,
                  DataAccessException {
            Dialect dialect = Dialect.getDialect(configuration.getProperties());
            DatabaseMetadata metadata = new DatabaseMetadata(connection, dialect);
            String schemaCreationOutput = path(path(HIBERNATE_DATA_DIR, "scripts"),
                     "schema_update", Long.valueOf(System.currentTimeMillis()).toString() + ".sql");
            String[] sql = configuration.generateSchemaUpdateScript(dialect, metadata);
            if (updateScriptsConfiguration.isGenerateSchemaUpdateScript()) {
               File file = new File(schemaCreationOutput);
               try {
                  writeStringToFile(
                           file,
                           join(configuration.generateSchemaUpdateScript(dialect, metadata),
                                    LINE_SEPARATOR));
                  LOGGER.info("create schema script dumped to %s ", file.getCanonicalPath());
               } catch (IOException e) {
                  LOGGER.error(e);
                  if (RuntimeUtility.isJunit()) {
                     throw new NestableRuntimeException(e);
                  }
               }
            }
            Statement stmt = null;
            try {
               stmt = connection.createStatement();
               for (String sqlStmt : sql) {
                  try {
                     stmt.executeUpdate(sqlStmt);
                  } catch (SQLException ex) {
                     LOGGER.warn("Unsuccessful schema statement: %s = %s", sqlStmt, ex);
                  }
               }
            } finally {
               JdbcUtils.closeStatement(stmt);
            }
            return null;
         }
      });
   }

   private HibernateSessionUtility() {
   }
}
